from scapy.all import Ether, IP, UDP, BOOTP, DHCP, sendp, sniff

def send_dhcp_discover():
    """
    构造并发送一个 DHCP Discover 报文。
    """
    # 以太网帧：广播地址 (ff:ff:ff:ff:ff:ff)
    # 目标 MAC 地址必须是广播地址，因为我们不知道 DHCP 服务器的 MAC 地址。
    eth = Ether(dst="ff:ff:ff:ff:ff:ff")

    # IP 报文：源地址 0.0.0.0 (未分配)，目标地址 255.255.255.255 (广播)
    ip = IP(src="0.0.0.0", dst="255.255.255.255")

    # UDP 报文：源端口 68 (DHCP 客户端)，目标端口 67 (DHCP 服务器)
    udp = UDP(sport=68, dport=67)

    # BOOTP 报文：DHCP 协议基于 BOOTP
    # op=1 表示 BOOTREQUEST（请求）
    # chaddr 为客户端的 MAC 地址（Scapy 会自动填入）
    bootp = BOOTP(op=1, xid=0x12345678, flags=0x8000)

    # DHCP 选项：message-type 为 1，表示 DHCP Discover
    dhcp_options = DHCP(options=[("message-type", "discover"), "end"])

    # 组合所有层
    dhcp_discover_packet = eth / ip / udp / bootp / dhcp_options

    print("正在发送 DHCP Discover 报文...")
    # sendp 在 L2 (第二层) 发送数据包，因为以太网帧是 L2
    sendp(dhcp_discover_packet, verbose=False)


def handle_dhcp_offer(packet):
    """
    处理捕获到的 DHCP Offer 报文。
    """
    # 检查数据包是否为 DHCP 且是 DHCP Offer
    if BOOTP in packet and DHCP in packet:
        try:
            # 获取 DHCP 选项
            options = packet[DHCP].options
            # 遍历选项，查找 message-type
            for item in options:
                if isinstance(item, tuple) and item[0] == "message-type" and item[1] == 2:
                    # message-type 为 2 表示 DHCP Offer
                    # BOOTP 报文中的 siaddr 是 DHCP 服务器的 IP 地址
                    server_ip = packet[BOOTP].siaddr
                    print("-" * 40)
                    print(f"检测到 DHCP Offer 报文！")
                    print(f"DHCP 服务器 IP 地址: {server_ip}")
                    print(f"服务器 MAC 地址: {packet[Ether].src}")
                    print("-" * 40)
                    # 返回 True 以停止嗅探
                    return True
        except IndexError:
            # 忽略没有 BOOTP 或 DHCP 层的非 DHCP 报文
            pass

    return False

if __name__ == "__main__":
    send_dhcp_discover()

    print("正在监听 DHCP Offer 报文...")
    # sniff 函数捕获网络数据包
    # filter 过滤器只捕获发往 68 端口（DHCP 客户端）的 UDP 报文
    # prn 回调函数，处理每个捕获到的数据包
    # stop_filter 函数，在回调函数返回 True 时停止嗅探
    sniff(filter="udp and port 68", stop_filter=handle_dhcp_offer, timeout=5)

    print("监听结束。")
